/*
 * Copyright [2009] [Marcin Rzeźnicki]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
 */
package mr.go.yaxc;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

import mr.go.yaxc.actions.*;

/**
 * Global configuration and registration of actions
 * 
 * <h6>Defaults</h6>
 * <table>
 * <tr>
 * <th>Action</th>
 * <th>Pattern</th>
 * <th>Matching</th>
 * <th>Priority</th>
 * </tr>
 * <tbody>
 * <tr>
 * <td>{@link CallMethod}</td>
 * <td>/Call</td>
 * <td>{@link ActionMatch#ENDS_WITH}</td>
 * <td>{@link #PRIORITY_HEAVILY_USED}</td>
 * </tr>
 * <tr>
 * <td>{@link CreateString}</td>
 * <td>/String</td>
 * <td>{@code ActionMatch.ENDS_WITH}</td>
 * <td>{@code PRIORITY_HEAVILY_USED}</td>
 * </tr>
 * <tr>
 * <td>{@link New}</td>
 * <td>/New</td>
 * <td>{@code ActionMatch.ENDS_WITH}</td>
 * <td>{@code PRIORITY_HEAVILY_USED}</td>
 * </tr>
 * <tr>
 * <td>{@link CreateBoolean}</td>
 * <td>/Boolean</td>
 * <td>{@code ActionMatch.ENDS_WITH}</td>
 * <td>{@link #PRIORITY_HEAVILY_USED}</td>
 * </tr>
 * <tr>
 * <td>{@link CreateInt}</td>
 * <td>/Integer</td>
 * <td>{@code ActionMatch.ENDS_WITH}</td>
 * <td>{@code PRIORITY_FREQUENT}</td>
 * </tr>
 * <tr>
 * <td>{@link Ref}</td>
 * <td>/Ref</td>
 * <td>{@code ActionMatch.ENDS_WITH}</td>
 * <td>{@code PRIORITY_FREQUENT}</td>
 * </tr>
 * <tr>
 * <td>{@link SetProperty}</td>
 * <td>/Properties</td>
 * <td>{@link ActionMatch#CHILD_OF}</td>
 * <td>{@code PRIORITY_FREQUENT}</td>
 * </tr>
 * <tr>
 * <td>{@link Constructor}</td>
 * <td>/Create/Constructor</td>
 * <td>{@code ActionMatch.ENDS_WITH}</td>
 * <td>{@link #PRIORITY_NORMAL}</td>
 * </tr>
 * <tr>
 * <td>{@link CreateList}</td>
 * <td>/List</td>
 * <td>{@code ActionMatch.ENDS_WITH}</td>
 * <td>{@code PRIORITY_NORMAL}</td>
 * </tr>
 * <tr>
 * <td>{@link CreateDouble}</td>
 * <td>/Double</td>
 * <td>{@code ActionMatch.ENDS_WITH}</td>
 * <td>{@link #PRIORITY_SELDOM_USED}</td>
 * </tr>
 * <tr>
 * <td>{@link CreateFloat}</td>
 * <td>/Float</td>
 * <td>{@code ActionMatch.ENDS_WITH}</td>
 * <td>{@code PRIORITY_SELDOM_USED}</td>
 * </tr>
 * <tr>
 * <td>{@link Create}</td>
 * <td>/Create</td>
 * <td>{@code ActionMatch.ENDS_WITH}</td>
 * <td>{@code PRIORITY_SELDOM_USED}</td>
 * </tr>
 * <tr>
 * <td>{@link CreateArray}</td>
 * <td>/Array</td>
 * <td>{@code ActionMatch.ENDS_WITH}</td>
 * <td>{@code PRIORITY_SELDOM_USED}</td>
 * </tr>
 * <tr>
 * <td>{@link CreateMapEntry}</td>
 * <td>/Map/Entry</td>
 * <td>{@code ActionMatch.ENDS_WITH}</td>
 * <td>{@code PRIORITY_SELDOM_USED}</td>
 * </tr>
 * <tr>
 * <td>{@link CustomCode}</td>
 * <td>/Method</td>
 * <td>{@code ActionMatch.ENDS_WITH}</td>
 * <td>{@link #PRIORITY_UNCOMMON}</td>
 * </tr>
 * <tr>
 * <td>{@link CreateMap}</td>
 * <td>/Map</td>
 * <td>{@code ActionMatch.ENDS_WITH}</td>
 * <td>{@code PRIORITY_UNCOMMON}</td>
 * </tr>
 * </tbody>
 * </table>
 * 
 * @author Marcin Rzeźnicki
 * 
 */
public final class Actions {

    /**
     * Obtains action info for {@code actionClass}
     *
     * @param actionClass
     *            key type
     * @return corresponding {@code ActionInfo} or null
     */
    public static ActionInfo getActionInfo(Class<? extends Action> actionClass) {
        for (ActionInfo actionInfo : registeredActions) {
            if (actionInfo.getActionClass().equals(actionClass)) {
                return actionInfo;
            }
        }
        return null;
    }

    /**
     * Returns default action. Default action is created when no other action
     * can be matched to document's location (except for root of document which
     * is left unmatched)
     *
     * @return default action or null if none
     */
    public static ActionInfo getDefaultAction() {
        return defaultAction;
    }

    /**
     * XML Namespace prefix used for all actions' attributes, unless redefined
     * in {@link ActionInfo}
     *
     * @return xml namespace prefix or empty string
     */
    public static String getGlobalAttributePrefix() {
        return globalAttributePrefix;
    }

    /**
     * Global mapping of attributes' names. May be further redefined in
     * {@link ActionInfo}
     *
     * @return map from names used by actions to document's names
     */
    public static Map<String, String> getGlobalAttributesMap() {
        return globalAttributesMap;
    }

    /**
     * Registers action via its {@link ActionInfo}. If {@code actionInfo} is
     * found equal to some previously registered {@code ActionInfo}, the latter
     * will be unregistered
     *
     * @param actionInfo
     *            to be registered
     */
    public static void registerAction(ActionInfo actionInfo) {
        addActionInfo(actionInfo);
    }

    /**
     * Simplified version of the method. Pattern will default to
     * {@link Class#getSimpleName()} of {@code actionClass}, {@code matchType}
     * to {@link ActionMatch#ENDS_WITH}, {@code priority} to
     * {@link #PRIORITY_NORMAL}
     *
     * @see Actions#registerAction(Class, String, ActionMatch, int)
     * @param actionClass
     *            type of action to be registered
     */
    public static void registerAction(Class<? extends Action> actionClass) {
        addActionInfo(new IntrospectiveActionInfo(actionClass));
    }

    /**
     * Simplified version of the method. Pattern will default to
     * {@link Class#getSimpleName()} of {@code actionClass}
     *
     * @param actionClass
     *            type of action to be registered
     * @param matchType
     *            match type
     * @param priority
     *            priority
     */
    public static void registerAction(
            Class<? extends Action> actionClass,
            ActionMatch matchType,
            int priority) {
        addActionInfo(new IntrospectiveActionInfo(
                actionClass,
                matchType,
                priority));
    }

    /**
     * Simplified version of the method. Pattern will default to
     * {@link Class#getSimpleName()} of {@code actionClass}, {@code matchType}
     * to {@link ActionMatch#ENDS_WITH}
     *
     * @param actionClass
     *            type of action to be registered
     * @param priority
     *            priority
     */
    public static void registerAction(
            Class<? extends Action> actionClass,
            int priority) {
        addActionInfo(new IntrospectiveActionInfo(actionClass,
                                                  priority));
    }

    /**
     * Registers action via its type. Registration mechanism will try to
     * localize {@code ActionInfo} by searching for class in the same location
     * as {@code actionClass}, under name
     * <code>actionClass.getName() + "Info"</code>. If not found, default
     * implementation will be created with given arguments. If this {@code
     * actionInfo} (whether discovered or created) is found equal to some
     * previously registered {@code ActionInfo}, the latter will be unregistered
     *
     * @param actionClass
     *            type of action to be registered
     * @param defaultPattern
     *            matching pattern
     * @param matchType
     *            match type
     * @param priority
     *            priority
     * @see ActionInfo#ActionInfo(ActionMatch, String, int)
     */
    public static void registerAction(
            Class<? extends Action> actionClass,
            String defaultPattern,
            ActionMatch matchType,
            int priority) {
        addActionInfo(new IntrospectiveActionInfo(
                actionClass,
                matchType,
                defaultPattern,
                priority));
    }

    /**
     * Resets {@link #getDefaultAction() default action},
     * {@link #getGlobalAttributePrefix() attribute prefix},
     * {@link #getGlobalAttributesMap() attribute names} and all modifications
     * made to default actions. Additionally, unregisters custom actions
     */
    public static void resetConfiguration() {
        registeredActions.clear();
        defaultAction = null;
        globalAttributePrefix = "";
        globalAttributesMap = null;
        defaults();
    }

    /**
     * Sets default action: Default action is created when no other action can
     * be matched to document's location (except for root of document which is
     * left unmatched). If {@code defaultAction} is registered, this method will
     * unregister it (in other words - after choosing action as default action
     * it will never be encountered during parsing if any other action matches,
     * even if it would have matched some location due to its settings).
     *
     * @param defaultAction
     *            new default action
     */
    public static void setDefaultAction(ActionInfo defaultAction) {
        unregisterAction(defaultAction);
        Actions.defaultAction = defaultAction;
    }

    /**
     * Sets default action: Default action is created when no other action can
     * be matched to document's location (except for root of document which is
     * left unmatched). If {@code defaultAction} is registered, this method will
     * unregister it (in other words - after choosing action as default action
     * it will never be encountered during parsing if any other action matches,
     * even if it would have matched some location due to its settings). See
     * {@link #registerAction(Class, String, ActionMatch, int)} for details of
     * {@code ActionInfo} processing
     *
     * @param actionClass
     *            new default action's class
     */
    public static void setDefaultAction(Class<? extends Action> actionClass) {
        unregisterAction(actionClass);
        Actions.defaultAction = new IntrospectiveActionInfo(actionClass);
    }

    /**
     * Sets XML Namespace prefix used for all actions' attributes, unless
     * redefined in {@link ActionInfo}
     *
     * @param globalAttributePrefix
     *            XML namespace prefix for attributes
     */
    public static void setGlobalAttributePrefix(String globalAttributePrefix) {
        if (globalAttributePrefix == null) {
            throw new IllegalArgumentException("globalAttributePrefix == null");
        }
        Actions.globalAttributePrefix = globalAttributePrefix;
    }

    public static void setGlobalAttributesMap(
            Map<String, String> globalAttributesMap) {
        Actions.globalAttributesMap = globalAttributesMap;
    }

    /**
     * Unregisters action
     *
     * @param actionInfo
     *            to be unregistered
     */
    public static void unregisterAction(ActionInfo actionInfo) {
        registeredActions.remove(actionInfo);
    }

    /**
     * Unregisters action
     *
     * @param actionClass
     *            to be unregistered
     */
    public static void unregisterAction(Class<? extends Action> actionClass) {
        int unregisteredActionIdx = -1;
        int idx = 0;
        for (ActionInfo actionInfo : registeredActions) {
            if (actionInfo.getActionClass().equals(actionClass)) {
                unregisteredActionIdx = idx;
                break;
            }
            idx++;
        }
        if (unregisteredActionIdx != -1) {
            registeredActions.remove(unregisteredActionIdx);
        }
    }

    static Action create(String location) {
        actionsLoop:
        for (ActionInfo actionInfo : registeredActions) {
            switch (actionInfo.getMatchType()) {
                case EQUALS:
                    if (location.equals(actionInfo.getPattern())) {
                        return actionInfo.getImplementation();
                    }
                    break;
                case ENDS_WITH:
                    if (location.endsWith(actionInfo.getPattern())) {
                        return actionInfo.getImplementation();
                    }
                    break;
                case CONTAINS:
                    if (location.contains(actionInfo.getPattern())) {
                        return actionInfo.getImplementation();
                    }
                    break;
                case CHILD_OF:
                    int indexOfSlash = location.lastIndexOf('/');
                    if (indexOfSlash != -1) {
                        if (location.substring(0,
                                               indexOfSlash).endsWith(
                                actionInfo.getPattern())) {
                            return actionInfo.getImplementation();
                        }
                    }
                    break;
                case LEVEL:
                    int wantedLevel = actionInfo.getLevel();
                    int level = 0;
                    indexOfSlash = -1;
                    while (level < wantedLevel) {
                        indexOfSlash = location.indexOf('/',
                                                        indexOfSlash + 1);
                        if (indexOfSlash == -1) {
                            continue actionsLoop;
                        }
                        level++;
                    }
                    if (location.indexOf('/',
                                         indexOfSlash + 1) == -1) {
                        return actionInfo.getImplementation();
                    }
                    break;
                case CUSTOM:
                    if (actionInfo.getLocationMatcher().matches(location)) {
                        return actionInfo.getImplementation();
                    }
                    break;
            }
        }
        if (defaultAction != null && location.indexOf('/') != -1) {
            return defaultAction.getImplementation();
        }
        return null;
    }

    private static void addActionInfo(final ActionInfo e) {
        if (registeredActions.contains(e)) {
            unregisterAction(e);
        }
        int insertionIndex = Collections.binarySearch(registeredActions,
                                                      e);
        if (insertionIndex >= 0) {
            registeredActions.add(insertionIndex,
                                  e);
        } else {
            registeredActions.add(-(insertionIndex + 1),
                                  e);
        }
    }

    private static void defaults() {
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/Call",
                PRIORITY_HEAVILY_USED) {

            @Override
            public Class<? extends Action> getActionClass() {
                return CallMethod.class;
            }

            @Override
            public Action getImplementation() {
                return new CallMethod();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/Boolean",
                PRIORITY_FREQUENT) {

            @Override
            public Class<? extends Action> getActionClass() {
                return CreateBoolean.class;
            }

            @Override
            public Action getImplementation() {
                return new CreateBoolean();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/Double",
                PRIORITY_SELDOM_USED) {

            @Override
            public Class<? extends Action> getActionClass() {
                return CreateDouble.class;
            }

            @Override
            public Action getImplementation() {
                return new CreateDouble();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/Float",
                PRIORITY_SELDOM_USED) {

            @Override
            public Class<? extends Action> getActionClass() {
                return CreateFloat.class;
            }

            @Override
            public Action getImplementation() {
                return new CreateFloat();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/Integer",
                PRIORITY_FREQUENT) {

            @Override
            public Class<? extends Action> getActionClass() {
                return CreateInt.class;
            }

            @Override
            public Action getImplementation() {
                return new CreateInt();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/List",
                PRIORITY_NORMAL) {

            @Override
            public Class<? extends Action> getActionClass() {
                return CreateList.class;
            }

            @Override
            public Action getImplementation() {
                return new CreateList();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/String",
                PRIORITY_HEAVILY_USED) {

            @Override
            public Class<? extends Action> getActionClass() {
                return CreateString.class;
            }

            @Override
            public Action getImplementation() {
                return new CreateString();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/Method",
                PRIORITY_UNCOMMON) {

            @Override
            public Class<? extends Action> getActionClass() {
                return CustomCode.class;
            }

            @Override
            public Action getImplementation() {
                return new CustomCode();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/New",
                PRIORITY_HEAVILY_USED) {

            @Override
            public Class<? extends Action> getActionClass() {
                return New.class;
            }

            @Override
            public Action getImplementation() {
                return new New();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/Ref",
                PRIORITY_FREQUENT) {

            @Override
            public Class<? extends Action> getActionClass() {
                return Ref.class;
            }

            @Override
            public Action getImplementation() {
                return new Ref();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.CHILD_OF,
                "/Properties",
                PRIORITY_FREQUENT) {

            @Override
            public Class<? extends Action> getActionClass() {
                return SetProperty.class;
            }

            @Override
            public Action getImplementation() {
                return new SetProperty();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/Create",
                PRIORITY_SELDOM_USED) {

            @Override
            public Class<? extends Action> getActionClass() {
                return Create.class;
            }

            @Override
            public Action getImplementation() {
                return new Create();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/Create/Constructor",
                PRIORITY_NORMAL) {

            @Override
            public Class<? extends Action> getActionClass() {
                return Constructor.class;
            }

            @Override
            public Action getImplementation() {
                return new Constructor();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/Array",
                PRIORITY_SELDOM_USED) {

            @Override
            public Class<? extends Action> getActionClass() {
                return CreateArray.class;
            }

            @Override
            public Action getImplementation() {
                return new CreateArray();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/Map",
                PRIORITY_UNCOMMON) {

            @Override
            public Class<? extends Action> getActionClass() {
                return CreateMap.class;
            }

            @Override
            public Action getImplementation() {
                return new CreateMap();
            }
        });
        Actions.registerAction(new ActionInfo(
                ActionMatch.ENDS_WITH,
                "/Map/Entry",
                PRIORITY_SELDOM_USED) {

            @Override
            public Class<? extends Action> getActionClass() {
                return CreateMapEntry.class;
            }

            @Override
            public Action getImplementation() {
                return new CreateMapEntry();
            }
        });
    }
    /**
     * Descriptive flag for priorities
     */
    public static final int PRIORITY_FREQUENT = 20;
    /**
     * Descriptive flag for priorities
     */
    public static final int PRIORITY_HEAVILY_USED = 10;
    /**
     * Descriptive flag for priorities
     */
    public static final int PRIORITY_MAX = 0;
    /**
     * Descriptive flag for priorities
     */
    public static final int PRIORITY_NORMAL = 50;
    /**
     * Descriptive flag for priorities
     */
    public static final int PRIORITY_SELDOM_USED = 80;
    /**
     * Descriptive flag for priorities
     */
    public static final int PRIORITY_UNCOMMON = 100;
    private static ActionInfo defaultAction;
    private static String globalAttributePrefix = "";
    private static Map<String, String> globalAttributesMap;
    private static List<ActionInfo> registeredActions =
            new ArrayList<ActionInfo>();

    static {
        defaults();
    }
}
